/*
*    swsyd - swsy daemon, make nsswitch easy
*    Copyright (C) 2024  Marcus Pedersén marcus@marcux.org
*
*    This program is free software: you can redistribute it and/or modify
*    it under the terms of the GNU General Public License as published by
*    the Free Software Foundation, either version 3 of the License, or
*    (at your option) any later version.
*
*    This program is distributed in the hope that it will be useful,
*    but WITHOUT ANY WARRANTY; without even the implied warranty of
*    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*    GNU General Public License for more details.
*
*    You should have received a copy of the GNU General Public License
*    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

package main

import (
	"crypto/rand"
	"os"
	"fmt"
	"log"
	"log/slog"
	"strconv"
	"strings"
	"regexp"
	"net"
	"net/http"
	"path/filepath"
	"gorm.io/gorm"
	"github.com/gin-gonic/gin"
	"github.com/tredoe/osutil/user/crypt/sha512_crypt"
	"notabug.org/marcux/swsy/swsyd/common"
	"notabug.org/marcux/swsy/swsyd/sql"
)


const VERSION string = "0.0.1"
const AUTHORS string = "Marcus Pedersén"

// Response message on post request
type PostResponse struct {
	Message string
}


// Struct holds handle
// to database and
// describes the API
type swsydAPI struct {
	DB *gorm.DB
	conf common.Config
}


// Returns users matching
// url query parameters
// name or uid. If both
// are given uid will be
// used. If name is empty
// string and search is specified
// all users are
// returned, same as no
// parameters.
// If search is not specified
// and name is empty empty array
// is returned. Name with no search
// the exact matching user is returned
// if no user is found empty array
// is returned.
// API:
// GET URL: /user
// Get all users
// GET URL: /user?uid=123
// Get user with uid 123
// GET URL: /user?name=abc
// Get user with name abc
// URL: /user?name=abc&search
// Get users starting with abc
func (s swsydAPI) getUser(c *gin.Context) {
	name, exName := c.GetQuery("name")
	uid, exUid := c.GetQuery("uid")
	_, exSearch := c.GetQuery("search")

	if exUid {
		// Get user with uid
		u, err := strconv.Atoi(uid)

		if err != nil {
			slog.Error(fmt.Sprintf("Failed to convert string: %s to int: %s", uid, err))
			users := []sql.Password{}
			c.JSON(http.StatusOK, users)
			return
		}

		users := sql.GetUser(s.DB, "", int32(u), false)
		c.JSON(http.StatusOK, users) 
	} else if exName {
		// Get user with exact name if not search
		// Get user with name like if search
		users := sql.GetUser(s.DB, name, -1, exSearch)
		c.JSON(http.StatusOK, users)
		return
	} else {
		// Get all users
		users := sql.GetUser(s.DB, "", -1, true)
		c.JSON(http.StatusOK, users)
	}
}


// API:
// GET URL: /group
// Get all groups
// GET URL: /group?gid=123
// Get group with gid 123
// GET URL: /group?name=abc
// Get group with name abc
// GET URL: /group?name=abc&search
// Get groups starting with abc
// GET URL: /group?user=abc
// Get all groups that user with username
// belongs to
// GET URL: /group?uid=123
// Get all groups that user with uid
// belongs to.
// Any other parameter combinations
// returns an empty array.
func (s swsydAPI) getGroup(c *gin.Context) {
	name, exName := c.GetQuery("name")
	gid, exGid := c.GetQuery("gid")
	_ , exSearch := c.GetQuery("search")
	uid, exUid := c.GetQuery("uid")
	user, exUser := c.GetQuery("user")

	if ! exName && ! exGid  && ! exSearch && ! exUid && ! exUser {
		// Get all groups
		fmt.Println("getting all groups")
		groups := sql.GetGroup(s.DB, "", -1, true)
		c.JSON(http.StatusOK, groups)
	} else if exName && ! exGid  && ! exUid && ! exUser {
		// Get group with name
		groups := sql.GetGroup(s.DB, name, -1, exSearch)
		c.JSON(http.StatusOK, groups)
	} else if exGid && ! exName && ! exSearch && ! exUid && ! exUser {
		// Get group with gid
		igid, err := strconv.Atoi(gid)
		if err != nil {
			slog.Error(fmt.Sprintf("getGroup: Failed to convert gid: %s to int. %s\n", gid, err))
			c.JSON(http.StatusOK, []sql.Group{})
		}
		
		groups := sql.GetGroup(s.DB, "", int32(igid), false)
		c.JSON(http.StatusOK, groups)
	} else if exUid && ! exGid && ! exSearch && ! exName && ! exUser {
		// Get groups for user with uid
		iuid, err := strconv.Atoi(uid)
		if err != nil {
			slog.Error(fmt.Sprintf("getGroup: Failed to convert uid: %s to int. %s\n", uid, err))
			c.JSON(http.StatusOK, []sql.Group{})
		} else {
			groups := sql.GetUserGroups(s.DB, "", int32(iuid))
			c.JSON(http.StatusOK, groups)
		}

	} else if exUser && ! exGid && ! exSearch && ! exUid && ! exName {
		// Get groups for username
		groups := sql.GetUserGroups(s.DB, user, -1)
		c.JSON(http.StatusOK, groups)

	} else {
		c.JSON(http.StatusOK, []sql.Group{})
	}
}



// API:
// GET URL: /shadow
// Get all shadow fields
// GET URL: /shadow?user=abc
// Get shadow fields for user
// with username
// GET URL: /shadow?user=abc&search
// Get shadow fields for users
// starting with name abc
// GET URL: /shadow?uid=123
// Get shadow fields for user with uid
// Any other parameter combinations
// returns an empty array.
func (s swsydAPI) getShadow(c *gin.Context) {
	uid, exUid := c.GetQuery("uid")
	user, exUser := c.GetQuery("user")
	_ , exSearch := c.GetQuery("search")

	shadows := []sql.Shadow{}
	
	if !exUid && !exUser && !exSearch {
		// Get all shadow records
		shadows = sql.GetShadow(s.DB, "", -1, true)
	} else if exUid && !exUser && !exSearch {
		// Get shafow fields for user with uid
		iuid, err := strconv.Atoi(uid)
		if err != nil {
			slog.Error(fmt.Sprintf("GetShadow: Failed to convert uid string: %s to int. %s", uid, err))
		} else {
			shadows = sql.GetShadow(s.DB, "", int32(iuid), exSearch)
		}
	} else if exUser && !exUid {
		// Get shadow with username or username staring with user
		shadows = sql.GetShadow(s.DB, user, -1, exSearch)
	}

	c.JSON(http.StatusOK, shadows)
}



// API:
// GET URL: /host
// Get all hosts
// GET URL: /host?name=abc
// Get host with hostname name
// GET URL: /host?name=abc&search
// Get host weith hostname
// starting with name abc
// GET URL: /host?ipv4=1.2.3.4
// Get host with IP version 4 number
// GET URL: /host?ipv6=1234:fd2:5621:1:89::4500
// Get host with IP version 6 number
// Any other parameter combinations
// returns an empty array.
func (s swsydAPI) getHost(c *gin.Context) {
	name, exName := c.GetQuery("name")
	ipv4, exIpv4 := c.GetQuery("ipv4")
	ipv6, exIpv6 := c.GetQuery("ipv6")
	_ , exSearch := c.GetQuery("search")

	hosts := []sql.Host{}

	if !exName && !exSearch && !exIpv4 && !exIpv6 {
		// Get all hosts
		hosts = sql.GetHost(s.DB, "", "", "", true)
	} else 	if exName && !exSearch && !exIpv4 && !exIpv6{
		// Get host with name
		hosts = sql.GetHost(s.DB, name, "", "", false)
	} else 	if exName && exSearch && !exIpv4 && !exIpv6 {
		// Get host starting with name
		hosts = sql.GetHost(s.DB, name, "", "", true)
	} else 	if exIpv4 && !exIpv6 && !exName && !exSearch {
		// Get host with IPv4
		hosts = sql.GetHost(s.DB, "", ipv4, "", false)
	} else 	if exIpv6 && !exIpv4 && !exName && !exSearch {
		// Get host with IPv6
		hosts = sql.GetHost(s.DB, "", "", ipv6, false)
	}

	c.JSON(http.StatusOK, hosts)
}


// Add user
// API:
// POST URL: /user
// With the following form fields:
// username: Required string
// uid: optional positive integer
// gid: optional positive integer
// gecos: optional string
// home: optional string (path)
// shell: optional string (path to shell)
func (s swsydAPI) postUser(c *gin.Context) {
	response := PostResponse{}
	u := sql.Password{}
	
	err := c.Bind(&u)
	if err != nil {
		slog.Error(fmt.Sprintf("postUser: Failed to parse supplied form to user: %s", err))
		response.Message = fmt.Sprintf("Failed to parse supplied form: %s", err)
		c.JSON(http.StatusBadRequest, response)
	} else if len(u.Username) == 0 {
		response.Message = "username is required"
		c.JSON(http.StatusBadRequest, response)
	}

	if len(u.Home) == 0 {
		u.Home = filepath.Join(s.conf.DefaultHome, u.Username)
	}

	if len(u.Shell) == 0 {
		u.Shell = s.conf.DefaultShell
	}

	if len(u.Password) != 0 {
		u.Password = ""
	}

	if err := sql.InsertUser(s.DB, s.conf, u); err != nil {
		response.Message = fmt.Sprintf("%s",err)
		c.JSON(http.StatusInternalServerError, response)
	}

	response.Message = fmt.Sprintf("User: %s successfully created", u.Username)
	c.JSON(http.StatusCreated, response)
}




// Add group
// API:
// POST URL: /group
// With the following form fields:
// groupname: Required string
// gid: optional positive integer
// password: clear text string
func (s swsydAPI) postGroup(c *gin.Context) {
	response := PostResponse{}
	g := sql.Group{}

	err := c.Bind(&g)

	if err != nil {
		slog.Error(fmt.Sprintf("postGroup: Failed to parse group from form: %s", err))
		response.Message = fmt.Sprintf("Failed to parse form: %s", err)
		c.JSON(http.StatusBadRequest, response)
		return
	} else if len(g.Groupname) == 0 {
		response.Message = "groupname is required"
		c.JSON(http.StatusBadRequest, response)
		return
	}

	if len(g.GroupPassword) > 0 {
		hash, err := passwordSha512(g.GroupPassword)
		if err != nil {
			slog.Error(fmt.Sprintf("%s", err))
			response.Message = "Failed to hash password"
			c.JSON(http.StatusInternalServerError, response)
			return
		} else {
			g.GroupPassword = hash
		}
	}

	err = sql.InsertGroup(s.DB, s.conf, g)
	if err != nil {
		slog.Error("Failed to save group: %s, to database: %s", g, err)
		response.Message = fmt.Sprintf("%s", err)
		c.JSON(http.StatusInternalServerError, response)
		return
	}

	response.Message = "Group successfully added."
	c.JSON(http.StatusOK, response)
}


// Add swadow
// API:
// POST URL: /shadow
// With the following form fields:
// username: Required string if not UID
// uid: Required integer if not username
// password: clear text string
// lastchange: optional integer
// minpasswordage: optional integer
// maxpasswordage: optional integer
// warningperiod: optional integer
// inactiveperiod: optional integer
// expiredate: string formated as date
func (s swsydAPI) postShadow(c *gin.Context) {
	response := PostResponse{}
	sh := sql.Shadow{}
	uid := -1
	uidStr := c.PostForm("uid")
	
	err := c.Bind(&sh)

	if err != nil {
		slog.Error(fmt.Sprintf("postShadow: Failed to parse shadow from form: %s", err))
		response.Message = fmt.Sprintf("Failed to parse form: %s", err)
		c.JSON(http.StatusBadRequest, response)
		return
	} else if len(sh.Username) == 0 && len(uidStr) == 0 {
		response.Message = "username or uid is required"
		c.JSON(http.StatusBadRequest, response)
		return
	} 

	if len(uidStr) > 0 {
		uid, err = strconv.Atoi(uidStr)
		if err != nil {
			slog.Error(fmt.Sprintf("postShadow: Failed to convert uid: %s to int: %s", uidStr, err))
			response.Message = fmt.Sprintf("Failed to convert uid: %s to integer.", uidStr)
			c.JSON(http.StatusBadRequest, response)
			return
		}
	}
	
	if len(sh.Password) > 0 {
		hash, err := passwordSha512(sh.Password)
		if err != nil {
			slog.Error(fmt.Sprintf("postShadow: %s", err))
			response.Message = "Failed to hash password"
			c.JSON(http.StatusInternalServerError, response)
			return
		} else {
			sh.Password = hash
		}
	}

	err = sql.InsertShadow(s.DB, s.conf, sh, int32(uid))
	if err != nil {
		slog.Error("Failed to save shadow: %s, to database: %s", sh, err)
		response.Message = fmt.Sprintf("%s", err)
		c.JSON(http.StatusInternalServerError, response)
		return
	}

	response.Message = "Password successfully added."
	c.JSON(http.StatusOK, response)
}



// Add host
// API:
// POST URL: /host
// With the following form fields:
// ipv4: IPv4 address, eg 192.168.1.16
// ipv6: IPv6 address
// hostnames: string with space separated hostnames, required
func (s swsydAPI) postHost(c *gin.Context) {
	response := PostResponse{}
	h := sql.Host{}
	
	err := c.Bind(&h)

	if err != nil {
		slog.Error(fmt.Sprintf("postHost: Failed to parse host from form: %s", err))
		response.Message = fmt.Sprintf("Failed to parse form: %s", err)
		c.JSON(http.StatusBadRequest, response)
		return
	}

	if len(h.Hostnames) == 0 {
		slog.Error("postHost: hostnames is required")
		response.Message = "Failed to add host, hostnames is required."
		c.JSON(http.StatusBadRequest, response)
		return
	}

	if len(h.IPv4) > 0 {
		ip4 := net.ParseIP(h.IPv4)
		if ip4 == nil {
			slog.Error(fmt.Sprintf("postHost: IPv4: %s, is not a valid IP address", h.IPv4))
			response.Message = fmt.Sprintf("Failed to add host, IPv4: %s is not a valid IP address.", h.IPv4)
			c.JSON(http.StatusBadRequest, response)
			return
		}
	}

	if len(h.IPv6) > 0 {
		ipv6 := net.ParseIP(h.IPv6)
		if ipv6 == nil {
			slog.Error(fmt.Sprintf("postHost: IPv6: %s, is not a valid IP address", h.IPv6))
			response.Message = fmt.Sprintf("Failed to add host, IPv6; %s is not a valid IP address", h.IPv6)
			c.JSON(http.StatusBadRequest, response)
			return
		}
	}


	h.Hostnames = strings.TrimSpace(h.Hostnames)

	regNames := regexp.MustCompile("^[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]$")

	// TODO
	// clean up muliple hosts, remove multiple spaces between names
	hostnames := ""
	for _, n := range strings.Fields(h.Hostnames) {
		if ! regNames.MatchString(n) {
			slog.Error("postHost: Hostnasme: %s is an invalid hostname", n)
			response.Message = fmt.Sprintf("Hostname: %s is invalid, name must start and end with alpanumeric characters and name can only contain \"-\", \".\" and alpanumeric characters.", n)
			c.JSON(http.StatusBadRequest, response)
			return
		}

		if len(hostnames) == 0 {
			hostnames = n
		} else {
			hostnames = fmt.Sprintf("%s %s", hostnames, n)
		}
	}

	h.Hostnames = hostnames
	err = sql.InsertHost(s.DB, s.conf, h)

	if err != nil {
		slog.Error(fmt.Sprintf("postHost: Failed to save host: %+v to database: %s", h, err))
		response.Message = fmt.Sprintf("%s", err)
		c.JSON(http.StatusInternalServerError, response)
		return
	}

	response.Message = "Host succesfully added."
	c.JSON(http.StatusOK, response)
}


// Hash password according to
// unix shadow file
// sha512
func passwordSha512(pwd string) (string, error) {
	const saltSize = 16
	var salt = make([]byte, saltSize)
	_, err := rand.Read(salt[:])
	if err != nil {
		return "", fmt.Errorf("Failed to generate salt for sha512 hash password: %s", err) 
	}

	salt = []byte(fmt.Sprintf("$6$%x", salt))

	c := sha512_crypt.New()
	hash, err := c.Generate([]byte(pwd), salt)
	if err != nil {
		return "", fmt.Errorf("Failed to generate password hash sha512")
	}

	return string(hash), nil
}


// Prints version to stdout
func printVersion() {
	fmt.Println("swsyd", VERSION)
	fmt.Printf("Copyright (C) 2024 %s.\n", AUTHORS)
	fmt.Println("License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>")
	fmt.Println("This is free software: you are free to change and redistribute it.")
	fmt.Println("There is NO WARRANTY, to the extent permitted by law.")
	fmt.Println("")
	fmt.Printf("Written by %s.\n", AUTHORS)
}


// Prints help text to stdout
func printHelp() {
	fmt.Println("Usage: swsyd [OPTION]...")
	fmt.Println("swsy daemon, make nsswitch easy")
	fmt.Println("")
	fmt.Println("Without any arguments swsyd will start in daemon mode.")
	fmt.Println("Deamon responds both to nsswitch clients and admin")
	fmt.Println("client that manages users, groups and hosts.")
	fmt.Println("")
	fmt.Println("Options:")
	fmt.Println("  -h, --help     Prints this helptext and exit.")
	fmt.Println("  -V, --version  Prints version information and exit.")
}


// Initialize log
func initLog(c common.Config) {
	logHandle, err := os.OpenFile(c.Logfile,
		os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)

	if err != nil {
		fmt.Fprintf(os.Stderr, "Failed to open logfile: %s, printing log to Stdout.\n%s\n",
			c.Logfile, err)
	} else {
		log.SetOutput(logHandle)
	}

	slog.SetLogLoggerLevel(c.Loglevel)
}


// Program start
func main() {
	if len(os.Args) >= 3 {
		fmt.Fprintf(os.Stderr, "Wrong arguments.\nTry -h or --help for help.\n")
		os.Exit(1)
	} else if len(os.Args) == 2 {
		if os.Args[1] == "--version" || os.Args[1] == "-V" {
			printVersion()
			os.Exit(0)
		} else if os.Args[1] == "--help" || os.Args[1] == "-h" {
			printHelp()
			os.Exit(0)
		} else {
			fmt.Fprintf(os.Stderr, "Wrong arguments.\nTry -h or --help for help.\n")
			os.Exit(1)
		}
	}

	conf := common.GetConfig()
	initLog(conf)
	slog.Info("Starting swsyd")

	swsyd := swsydAPI{}
	db, err := sql.InitDb(conf)
	if err != nil {
		slog.Error(fmt.Sprintf("Failed to initialize database: %s", conf.Dbfile))
		slog.Error(fmt.Sprintf("%s", err))
		slog.Error("Terminating swsyd")
		os.Exit(3)
	}

	swsyd.conf = conf
	swsyd.DB = db
	defer func() {
		if db, err := swsyd.DB.DB(); err != nil {
			slog.Error("Failed  to close database connection:")
			slog.Error(fmt.Sprintf("%s", err))
		} else {
			db.Close()
		}
	}()
	
	g := gin.Default()
	g.GET("/user", swsyd.getUser)
	g.GET("/group", swsyd.getGroup)
	g.GET("/shadow", swsyd.getShadow)
	g.GET("/host",swsyd.getHost)
	g.POST("/user", swsyd.postUser)
	g.POST("/group", swsyd.postGroup)
	g.POST("/shadow", swsyd.postShadow)
	g.POST("/host", swsyd.postHost)
	g.Run(fmt.Sprintf(":%d", conf.Port))

}
